/**  
 * Copyright (c) 2008, Noel Lynch All rights reserved. Redistribution and use  
 * in source and binary forms, with or without modification, are permitted  
 * provided that the following conditions are met: Redistributions of source  
 * code must retain the above copyright notice, this list of conditions and the  
 * following disclaimer. Redistributions in binary form must reproduce the above  
 * copyright notice, this list of conditions and the following disclaimer in the  
 * documentation and/or other materials provided with the distribution. Neither  
 * the name of Noel Lynch nor the names of contributors may be used to  
 * endorse or promote products derived from this software without specific prior  
 * written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 
 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT  
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A  
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR  
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,  
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,  
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;  
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,  
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR  
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF  
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  
 */
// Nov 7, 2008
package com.sceneworker.app.input;

import java.util.ArrayList;

import com.acarter.scenemonitor.SceneMonitor;
import com.jme.bounding.BoundingVolume;
import com.jme.input.KeyInput;
import com.jme.input.MouseInput;
import com.jme.intersection.BoundingPickResults;
import com.jme.intersection.PickResults;
import com.jme.intersection.TrianglePickData;
import com.jme.intersection.TrianglePickResults;
import com.jme.math.FastMath;
import com.jme.math.Quaternion;
import com.jme.math.Ray;
import com.jme.math.Triangle;
import com.jme.math.Vector2f;
import com.jme.math.Vector3f;
import com.jme.renderer.Camera;
import com.jme.renderer.Renderer;
import com.jme.scene.Spatial;
import com.jme.scene.TriMesh;
import com.jme.system.DisplaySystem;
import com.jme2utils.objects.AxesRep;
import com.jme2utils.objects.InputMode;
import com.jme2utils.staticutils.BoundsUtils;
import com.jme2utils.staticutils.GenericJMEUtils;
import com.jme2utils.staticutils.MeshUtils;
import com.sceneworker.app.events.DelegateEventTypes;
import com.sceneworker.app.globals.SceneWorkerGlobals;
import com.sceneworker.app.globals.cacheobjects.TexturePainterCacheObject;
import com.sceneworker.app.undo.GroupUndoAction;
import com.sceneworker.app.undo.SceneWorkerAppUndoManager;
import com.sceneworker.app.undo.SpatialScaleUndoAction;
import com.sceneworker.app.undo.SpatialTranslationUndoAction;
import com.sceneworker.util.delegation.DelegationEvent;
import com.sceneworker.util.delegation.DelegationEventBroadcaster;

public class SpatialSelectionHandler extends BaseSelectionHandler{
	private		CameraHandler							mcl_camHandler;
	private		AlignmentHandler						mcl_aligner;
	private		SpatialTransformHandler					mcl_transformer;
	
	private	Spatial		mcl_currentSpatial;
	private Spatial 	mcl_lastSpatial;

    private ArrayList<Spatial> mcla_otherSelections;

    private float mf_rotateCam = 0.0f;
    
    private     boolean     mb_triangleSelectionMode;
    private     boolean     mb_trianglePaintMode;
    
    public SpatialSelectionHandler() {
        mcl_picks = new BoundingPickResults();
        mcl_picks.setCheckDistance(true);
        mcla_otherSelections = new ArrayList<Spatial>();
        mb_triangleSelectionMode = false;
        mb_trianglePaintMode = false;
        
        mcl_aligner = new AlignmentHandler();
        mcl_camHandler = new CameraHandler();
        mcl_transformer = new SpatialTransformHandler();
        mi_inputMode = InputMode.kObjectSelection;
    }
    
    @Override
	public	void orbitCamera() {
    	mcl_camHandler.orbitCamera(DisplaySystem.getDisplaySystem().getRenderer().getCamera(), getCameraReferencePt());
	}
    
    @Override
    public	void zoomCamera() {
    	mcl_camHandler.zoomCamera(DisplaySystem.getDisplaySystem().getRenderer().getCamera(), getCameraReferencePt());
	}

	private	Vector3f		getCameraReferencePt() {
		Vector3f		lcl_refPt = null;
		
		if(mcl_currentSpatial != null) {
			if(mcl_currentSpatial.getWorldBound() != null) {
				lcl_refPt = mcl_currentSpatial.getWorldBound().getCenter();
			} else {
				lcl_refPt = mcl_currentSpatial.getWorldTranslation();
			}
		} else {
			lcl_refPt = Vector3f.ZERO;
		}
		
		return lcl_refPt;
	}

	@Override
	public void setInputMode(InputMode clIp) {
    	mi_inputMode = clIp;
    	mb_focusOn = false;
	}

	public	void	setTrianglePaintMode(boolean b_triPaint) {
    	mb_trianglePaintMode = b_triPaint;
    }
    
    public	boolean	getTrianglePaintMode() {
    	return mb_trianglePaintMode;
    }
    
    public  void    setTriangleSelectMode(boolean b_triSel) {
        mb_triangleSelectionMode = b_triSel;
    }
    
    public  boolean getTriSelectMode() {
        return mb_triangleSelectionMode;
    }
    
    public PickResults getPickResults() {
        return mcl_picks;
    }

    public void clearOtherSelections() {
        mcla_otherSelections.clear();
    }

    public Object getCurrentSelection() {
        return mcl_currentSpatial;
    }

    public ArrayList<Spatial> getOtherSelections() {
        return mcla_otherSelections;
    }

    public void setCurrentSelection(Object cl_sel) {
    	if(cl_sel instanceof Spatial) {
    		mcl_lastSpatial = mcl_currentSpatial;
            mcl_currentSpatial = (Spatial)cl_sel;
            clearOtherSelections();
    	}
    }

    public void translate(float f_x, float f_y, float f_z) {
        if (mcl_currentSpatial == null) {
            return;
        }

        Vector3f lcl_tx = mcl_currentSpatial.getLocalTranslation();
        mcl_currentSpatial.setLocalTranslation(lcl_tx.x + f_x, lcl_tx.y + f_y,
                lcl_tx.z + f_z);
    }

    public void rotate(float f_delta, Vector3f cl_axis) {
        if (mcl_currentSpatial == null) {
            return;
        }

        // now depending on the axis add the delta to it and create a new
        // quaternion
        Quaternion lcl_newRot = new Quaternion();
        lcl_newRot.fromAngleAxis(f_delta, cl_axis);

        // multiply it to the existing rotation
        mcl_currentSpatial.getLocalRotation().multLocal(lcl_newRot);
    }

    public void rotateCamAboutSpatial() {
        Quaternion      lcl_q = new Quaternion();
        lcl_q.fromAngleAxis(mf_rotateCam, Vector3f.UNIT_Y.clone());
        
        Vector3f        lcl_spatialLoc = mcl_currentSpatial.getWorldTranslation().clone();
        BoundingVolume  lcl_v = mcl_currentSpatial.getWorldBound();
        
        if(lcl_v == null) {
            return;
        }
        
        float       lf_offset = BoundsUtils.getMaxInBound(lcl_v);
        
        Vector3f    lcl_heading = new Vector3f(0, 0, lf_offset * 4f);
        lcl_heading = lcl_q.mult(lcl_heading);
        
        lcl_heading.addLocal(lcl_v.getCenter());
        lcl_heading.y += 10f;
        
        Camera      lcl_cam = DisplaySystem.getDisplaySystem().getRenderer().getCamera();
        lcl_cam.setLocation(lcl_heading);
        lcl_cam.lookAt(lcl_v.getCenter(), Vector3f.UNIT_Y);
        lcl_cam.update();
        
        mf_rotateCam += CAM_DELTA;    
    }

    public void endRotateCamAboutSpatial() {
        mf_rotateCam = 0.0f;
    }

    public boolean spatialSelected(Spatial cl_spatial) {
        for (Spatial lcl_spatial : mcla_otherSelections) {
            if (lcl_spatial == cl_spatial) {
                return true;
            }
        }

        return false;
    }

    public boolean updatePickResults(Spatial cl_rootNode, Vector2f cl_screenPos,
            boolean b_multiSel, boolean b_removeSel) {
        
        if(!mb_triangleSelectionMode) {
            return selectSpatial(cl_rootNode, cl_screenPos, b_multiSel, b_removeSel);
        }
        
        return selectTriInSpatial(cl_screenPos);
        
    }
    
    protected   boolean selectTriInSpatial(Vector2f cl_screenPos) {
        if(mcl_currentSpatial == null) {
            return false;
        }
        
        TrianglePickResults     lcl_tripicks = new TrianglePickResults();
        
        Vector3f lcl_worldNear = DisplaySystem.getDisplaySystem()
        .getWorldCoordinates(cl_screenPos, 0);
        Vector3f lcl_worldFar = DisplaySystem.getDisplaySystem()
        .getWorldCoordinates(cl_screenPos, 1);

        Ray lcl_ray = new Ray(lcl_worldNear, lcl_worldFar.subtractLocal(
                lcl_worldNear).normalizeLocal());
        
        TrianglePickData        lcl_p = BoundsUtils.findClosestTriangle(lcl_ray, mcl_currentSpatial);
        
        if(lcl_p == null) {
            //System.out.println("no sel");
            return false;
        }
        
        //System.out.println(lcl_p.getTargetMesh().getName());
        
        if(lcl_p.getTargetMesh() instanceof TriMesh) {
            TriMesh     lcl_mesh = (TriMesh)lcl_p.getTargetMesh();
            
            if(lcl_p.getTargetTris().size() > 0) {
            	if(mb_trianglePaintMode) {
            		doTexturePaint(lcl_ray, lcl_mesh, lcl_p.getTargetTris().get(0));
            	}
            	
                DelegationEventBroadcaster.inst().broadcast(new DelegationEvent(DelegateEventTypes.NEW_TRI_SELECTION_EVT, this, lcl_mesh, lcl_p.getTargetTris().get(0), true));
            } else {
                DelegationEventBroadcaster.inst().broadcast(new DelegationEvent(DelegateEventTypes.NO_TRI_SELECTION_EVT, this));
            }
        }
        
        return true;
    }
    
    private	void	doTexturePaint(Ray cl_ray, TriMesh cl_mesh, int i_triIndex) {
    	// get the texture painter panel if it exists
    	TexturePainterCacheObject		lcl_co = (TexturePainterCacheObject)SceneWorkerGlobals.inst().getFromCache(TexturePainterCacheObject.TEXTURE_PAINTER);
    	if(lcl_co == null) {
    		return;
    	}
    	
    	// get the selected triangle
    	Triangle		lcl_selectedTri = MeshUtils.getWorldTriangle(cl_mesh, i_triIndex, null);
    	lcl_selectedTri.calculateNormal();
    	
    	// calculate intersection in triangle
    	Vector3f		lcl_intersection = new Vector3f();
    	if(!cl_ray.intersectWhere(lcl_selectedTri, lcl_intersection)) {
    		//System.out.println("no intersection...");
    		return;
    	}
    	
    	//System.out.println("pt of intersection " + GenericJMEUtils.vector3f2String(lcl_intersection));
    	
    	// get uv coords
    	Vector2f		lcla_uv[] = MeshUtils.getTextureCoordinates(cl_mesh, i_triIndex, 0);
    	//System.out.println(lcla_uv[0].toString() + " " + lcla_uv[1].toString() + " " + lcla_uv[2].toString());
    	
    	// compute barycentric coords
    	Vector3f		lcl_bary = GenericJMEUtils.computeBarycentricCoordinates(lcl_selectedTri, lcl_intersection);
    	
    	// compute interpolated texture coordinate
    	Vector2f		lcl_textureLoc = new Vector2f();
    	lcl_textureLoc.addLocal(lcla_uv[0].multLocal(lcl_bary.x));
    	lcl_textureLoc.addLocal(lcla_uv[1].multLocal(lcl_bary.y));
    	lcl_textureLoc.addLocal(lcla_uv[2].multLocal(lcl_bary.z));
    	
    	//System.out.println("texture location @ intersection = " + lcl_textureLoc + " with bary " + GenericJMEUtils.vector3f2String(lcl_bary));
    	
    	// get painter panel to paint at coord
    	lcl_co.getTextureListerPanel().paintAt(lcl_textureLoc);
    }
    
    protected   boolean selectSpatial(Spatial cl_rootNode, Vector2f cl_screenPos,
            boolean b_multiSel, boolean b_removeSel) {
        mcl_picks.clear();

        Spatial lcl_new = null;

        Vector3f lcl_worldNear = DisplaySystem.getDisplaySystem()
                .getWorldCoordinates(cl_screenPos, 0);
        Vector3f lcl_worldFar = DisplaySystem.getDisplaySystem()
                .getWorldCoordinates(cl_screenPos, 1);

        Ray lcl_ray = new Ray(lcl_worldNear, lcl_worldFar.subtractLocal(
                lcl_worldNear).normalizeLocal());
        cl_rootNode.findPick(lcl_ray, mcl_picks);

        if (mcl_picks.getNumber() > 0) {
            lcl_new = mcl_picks.getPickData(0).getTargetMesh();
        }

        if (b_removeSel) {
            if (spatialSelected(lcl_new)) {
                mcla_otherSelections.remove(lcl_new);
            }
            return false;
        }

        if (!b_multiSel) {
            if (mcl_currentSpatial != lcl_new) {
                mcl_lastSpatial = mcl_currentSpatial;
                mcl_currentSpatial = lcl_new;
                mcla_otherSelections.clear();
                return true;
            }
        } else {
            if (!spatialSelected(lcl_new)) {
                mcla_otherSelections.add(lcl_new);
            }
        }

        return false;
    }
    
    private	boolean	handleModifierPick(AxesRep cl_rep) {
    	System.out.println("modifier call");
		if(cl_rep == null) {
			return false;
		}
		
		Camera		lcl_cam = DisplaySystem.getDisplaySystem().getRenderer().getCamera();
		Vector3f	lcl_near = new Vector3f(lcl_cam.getWorldCoordinates(new Vector2f(MouseInput.get().getXAbsolute(), MouseInput.get().getYAbsolute()), 0f));
		Vector3f	lcl_far = new Vector3f(lcl_cam.getWorldCoordinates(new Vector2f(MouseInput.get().getXAbsolute(), MouseInput.get().getYAbsolute()), 1f));
		Ray			lcl_ray = new Ray(lcl_near, lcl_far.subtract(lcl_near));
		mi_inputMode = cl_rep.handlePick(lcl_ray, mi_inputMode);
		
		System.out.println("modifier call : input mode " + mi_inputMode);
		return true;
	}
	
	
	public	void	handleLeftMouseDown(Spatial cl_rootNode, AxesRep cl_curAxes) {
		if(mcl_currentSpatial != null) {
			if(mcl_currentSpatial.getRenderQueueMode() == Renderer.QUEUE_ORTHO) {
				return;
			}
		}
		
		if(!mb_mouseAlreadyDown) {
			handleModifierPick(cl_curAxes);
		}
		
		if(!mb_focusOn) {
			mb_focusOn = true;
			return;
		}
		
		Camera		lcl_cam = DisplaySystem.getDisplaySystem().getRenderer().getCamera();
		float		lf_xDelta;
		float		lf_yDelta;
		
		switch(mi_inputMode) {
		case	kObjectSelection:
			objectSelectionInputHandler(cl_rootNode);
			break;

		case	kObjectFreeTranslation:
			mcl_transformer.objectFreeTranslation(mcl_currentSpatial, mb_mouseAlreadyDown);
			break;

		case	kObjectTranslationX:
			lf_xDelta = -lcl_cam.getDirection().dot(Vector3f.UNIT_Z);
			lf_yDelta = lcl_cam.getUp().dot(Vector3f.UNIT_Y);
			
			objectTranslationInputHandler(Vector3f.UNIT_X, new Vector2f(FastMath.sign(lf_xDelta), FastMath.sign(lf_yDelta)));
			//objectTranslationInputHandler(Vector3f.UNIT_X, new Vector2f(1, 0));
			break;

		case	kObjectTranslationY:
			objectTranslationInputHandler(Vector3f.UNIT_Y, new Vector2f(0, 1));
			break;	

		case	kObjectTranslationZ:
			lf_xDelta = lcl_cam.getDirection().dot(Vector3f.UNIT_X);
			lf_yDelta = lcl_cam.getUp().dot(Vector3f.UNIT_Y);
			
			objectTranslationInputHandler(Vector3f.UNIT_Z, new Vector2f(FastMath.sign(lf_xDelta), FastMath.sign(lf_yDelta)));
			//objectTranslationInputHandler(Vector3f.UNIT_Z, new Vector2f(1, 1));
			break;
			
		case	kObjectRotateY:
			objectRotate(Vector3f.UNIT_Y);
			break;
			
		case	kObjectRotateZ:
			objectRotate(Vector3f.UNIT_Z);
			break;
			
		case	kObjectRotateX:
			objectRotate(Vector3f.UNIT_X);
			break;	
			
		case	kObjectScaleX:
			mcl_transformer.objectScale(mcl_currentSpatial, mb_mouseAlreadyDown, Vector3f.UNIT_X);
			break;
			
		case	kObjectScaleY:
			mcl_transformer.objectScale(mcl_currentSpatial, mb_mouseAlreadyDown, Vector3f.UNIT_Y);
			break;
			
		case	kObjectScaleZ:
			mcl_transformer.objectScale(mcl_currentSpatial, mb_mouseAlreadyDown, Vector3f.UNIT_Z);
			break;
			
		case	kTexturePaint:
			break;
		}
		
		if(!mb_mouseAlreadyDown) {
			mb_mouseAlreadyDown = true;
		}
	}
	
	private	void	objectSelectionInputHandler(Spatial cl_rootNode) {
		Vector2f		lcl_pt = new Vector2f(MouseInput.get().getXAbsolute(), MouseInput.get().getYAbsolute());
		
		boolean			lb_shiftDown = KeyInput.get().isKeyDown(KeyInput.KEY_LCONTROL);
		boolean			lb_controlDown = KeyInput.get().isKeyDown(KeyInput.KEY_LMENU);
		
		Spatial			lcl_oldSel = mcl_currentSpatial;
		
		if(updatePickResults(cl_rootNode, lcl_pt, lb_shiftDown, lb_controlDown)) {
			if(lcl_oldSel != mcl_currentSpatial) {
				DelegationEventBroadcaster.inst().broadcast(new DelegationEvent(DelegateEventTypes.NEW_SPATIAL_SELECTION_EVT, this, mcl_currentSpatial));
			}
			
			if(mcl_currentSpatial != null) {
				SceneMonitor.getMonitor().selectSceneObject(mcl_currentSpatial);
			} else {
				SceneMonitor.getMonitor().selectSceneObject(null);
				clearOtherSelections();
			}
		}
	}
	
	private		Spatial[]		othersToArray() {
		Spatial		lcla_o[] = new Spatial[mcla_otherSelections.size()];
		int			li_index = 0;
		for(Spatial lcl_s : mcla_otherSelections) {
			lcla_o[li_index++] = lcl_s;
		}
		
		return lcla_o;
	}
	
	private	void	objectTranslationInputHandler(Vector3f cl_txMod, Vector2f cl_mouseMod) {
		mcl_transformer.objectTranslationInputHandler(mcl_currentSpatial, mb_mouseAlreadyDown, cl_txMod, cl_mouseMod, othersToArray());
	}
	
	private	void	objectRotate(final Vector3f cl_unitVector) {
		mcl_transformer.objectRotate(mcl_currentSpatial, mb_mouseAlreadyDown, cl_unitVector, othersToArray());
	}
	
	static	public	void	initialiseUndoTranslation(Spatial cl_ref, Spatial ...cla_others) {
		GroupUndoAction		lcl_grp = new GroupUndoAction();
		lcl_grp.addUndoAction(new SpatialTranslationUndoAction(cl_ref, cl_ref.getLocalTranslation().clone()));
		
		for(Spatial lcl_other : cla_others) {
			lcl_grp.addUndoAction(new SpatialTranslationUndoAction(lcl_other, lcl_other.getLocalTranslation().clone()));
		}
		
		SceneWorkerAppUndoManager.inst().addUndoAction(lcl_grp);
	}
	
	static	private	void	initialiseUndoScale(Spatial cl_sel, Spatial ...cla_others) {
		GroupUndoAction		lcl_grp = new GroupUndoAction();
		lcl_grp.addUndoAction(new SpatialScaleUndoAction(cl_sel, cl_sel.getLocalScale().clone()));
		
		for(Spatial lcl_other : cla_others) {
			lcl_grp.addUndoAction(new SpatialScaleUndoAction(lcl_other, lcl_other.getLocalScale().clone()));
		}
		
		SceneWorkerAppUndoManager.inst().addUndoAction(lcl_grp);
	}
	
	public	void	alignAlongX() {
		mcl_aligner.alignAlongX(mcl_currentSpatial, mb_mouseAlreadyDown, othersToArray());
	}
	
	public	void	alignAlongY() {
		mcl_aligner.alignAlongY(mcl_currentSpatial, mb_mouseAlreadyDown, othersToArray());
	}
	
	public	void	alignAlongZ() {
		mcl_aligner.alignAlongZ(mcl_currentSpatial, mb_mouseAlreadyDown, othersToArray());
	}
	
	public	void	spaceAlongX() {
		mcl_aligner.spaceAlongX(mcl_currentSpatial, mb_mouseAlreadyDown, othersToArray());
	}
	
	public	void	spaceAlongZ() {
		mcl_aligner.spaceAlongZ(mcl_currentSpatial, mb_mouseAlreadyDown, othersToArray());
	}
	
	public	void	spaceAlongY() {
		mcl_aligner.spaceAlongY(mcl_currentSpatial, mb_mouseAlreadyDown, othersToArray());
	}
}
